diff options
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt')
-rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 569727b90..5b4bf2c9f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -7,20 +7,39 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.InstallableAdapter import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.ui.main.MainActivity +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.FileUtil +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.math.BigInteger +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter class InstallableFragment : Fragment() { private var _binding: FragmentInstallablesBinding? = null @@ -56,6 +75,17 @@ class InstallableFragment : Fragment() { binding.root.findNavController().popBackStack() } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.openImportSaves.collect { + if (it) { + importSaves.launch(arrayOf("application/zip")) + homeViewModel.setOpenImportSaves(false) + } + } + } + } + val installables = listOf( Installable( R.string.user_data, @@ -64,6 +94,43 @@ class InstallableFragment : Fragment() { export = { mainActivity.exportUserData.launch("export.zip") } ), Installable( + R.string.manage_save_data, + R.string.manage_save_data_description, + install = { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.import_save_warning, + descriptionId = R.string.import_save_warning_description, + positiveAction = { homeViewModel.setOpenImportSaves(true) } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + }, + export = { + val oldSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(false) + ) + val futureSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(true) + ) + if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) { + Toast.makeText( + YuzuApplication.appContext, + R.string.no_save_data_found, + Toast.LENGTH_SHORT + ).show() + return@Installable + } else { + exportSaves.launch( + "${getString(R.string.save_data)} " + + LocalDateTime.now().format( + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + ) + ) + } + } + ), + Installable( R.string.install_game_content, R.string.install_game_content_description, install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } @@ -121,4 +188,156 @@ class InstallableFragment : Fragment() { windowInsets } + + private val importSaves = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val inputZip = requireContext().contentResolver.openInputStream(result) + val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") + cacheSaveDir.mkdir() + + if (inputZip == null) { + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.save_files_importing, + false + ) { + try { + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) + val files = cacheSaveDir.listFiles() + var successfulImports = 0 + var failedImports = 0 + if (files != null) { + for (file in files) { + if (file.isDirectory) { + val baseSaveDir = + NativeLibrary.getSavePath(BigInteger(file.name, 16).toString()) + if (baseSaveDir.isEmpty()) { + failedImports++ + continue + } + + val internalSaveFolder = File( + "${DirectoryInitialization.userDirectory}/nand$baseSaveDir" + ) + internalSaveFolder.deleteRecursively() + internalSaveFolder.mkdir() + file.copyRecursively(target = internalSaveFolder, overwrite = true) + successfulImports++ + } + } + } + + withContext(Dispatchers.Main) { + if (successfulImports == 0) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.save_file_invalid_zip_structure, + descriptionId = R.string.save_file_invalid_zip_structure_description + ).show(parentFragmentManager, MessageDialogFragment.TAG) + return@withContext + } + val successString = if (failedImports > 0) { + """ + ${ + requireContext().resources.getQuantityString( + R.plurals.saves_import_success, + successfulImports, + successfulImports + ) + } + ${ + requireContext().resources.getQuantityString( + R.plurals.saves_import_failed, + failedImports, + failedImports + ) + } + """ + } else { + requireContext().resources.getQuantityString( + R.plurals.saves_import_success, + successfulImports, + successfulImports + ) + } + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.import_complete, + descriptionString = successString + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + + cacheSaveDir.deleteRecursively() + } catch (e: Exception) { + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + } + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + + private val exportSaves = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/zip") + ) { result -> + if (result == null) { + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.save_files_exporting, + false + ) { + val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") + cacheSaveDir.mkdir() + + val oldSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(false) + ) + if (oldSaveDataFolder.exists()) { + oldSaveDataFolder.copyRecursively(cacheSaveDir) + } + + val futureSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(true) + ) + if (futureSaveDataFolder.exists()) { + futureSaveDataFolder.copyRecursively(cacheSaveDir) + } + + val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0 + if (saveFilesTotal == 0) { + cacheSaveDir.deleteRecursively() + return@newInstance getString(R.string.no_save_data_found) + } + + val zipResult = FileUtil.zipFromInternalStorage( + cacheSaveDir, + cacheSaveDir.path, + BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) + ) + cacheSaveDir.deleteRecursively() + + return@newInstance when (zipResult) { + TaskState.Completed -> getString(R.string.export_success) + TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) + } + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } } |